package com.space.components.exception.catcher;

import com.alibaba.fastjson.JSON;
import com.space.components.exception.BaseException;
import com.space.components.exception.BizException;
import com.space.components.exception.SysException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author <a href="mailto:414563035@qq.com">Bedroom</a>
 * @date 2021/4/14 2:26 下午
 */
@Aspect
@Slf4j
public class ExceptionCatcherAspect {
    private Map<String, Class<?>> returnTypeMap = new ConcurrentHashMap<>();
    @Pointcut("@within(ExceptionCatcher) && execution(public * *(..))")
    public void pointcut() {
    }

    @Around(value = "pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        long startTime = System.currentTimeMillis();
        logRequest(joinPoint);

        Object response = null;
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Class<?> returnType = ms.getReturnType();

        if (isMono(returnType)) {
            try {
                response = joinPoint.proceed();
                response = ((Mono<Object>) response)
                        .onErrorResume(error -> Mono.just(handleException(getReactorReturnType(ms), error)))
                        .doOnNext(r -> logResponse(startTime, r));
            } catch (Throwable e) {
                response = Mono.just(handleException(getReactorReturnType(ms), e));
            }
        } else if (isFlux(returnType)) {
            try {
                response = joinPoint.proceed();
                response = ((Flux<Object>) response)
                        .onErrorResume(error -> Flux.just(handleException(getReactorReturnType(ms), error)))
                        .doOnNext(r -> logResponse(startTime, r));
            } catch (Throwable e) {
                response = Flux.just(handleException(getReactorReturnType(ms), e));
            }
        } else {
            try {
                response = joinPoint.proceed();
            } catch (Throwable e) {
                response = handleException(returnType, e);
            } finally {
                logResponse(startTime, response);
            }
        }

        return response;
    }

    private Object handleException(Class<?> returnType, Throwable e) {
        if (e instanceof BizException) {
            log.warn("BIZ EXCEPTION : " + e.getMessage());
            //在Debug的时候，对于BizException也打印堆栈
            if (log.isDebugEnabled()) {
                log.error(e.getMessage(), e);
            }
            return ResponseHandler.handle(returnType, (BaseException) e);
        }

        if (e instanceof SysException) {
            log.error("SYS EXCEPTION :");
            log.error(e.getMessage(), e);
            return ResponseHandler.handle(returnType, (BaseException) e);
        }

        log.error("UNKNOWN EXCEPTION :");
        log.error(e.getMessage(), e);

        return ResponseHandler.handle(returnType, "UNKNOWN_ERROR", e.getMessage());
    }


    private void logRequest(ProceedingJoinPoint joinPoint) {
        try {
            log.debug("START PROCESSING: " + joinPoint.getSignature().toShortString());
            Object[] args = joinPoint.getArgs();
            for (Object arg : args) {
                log.debug("REQUEST : " + JSON.toJSONString(arg));
            }
        } catch (Exception e) {
            //swallow it
            log.error("logReqeust error : " + e);
        }
    }

    private void logResponse(long startTime, Object response) {
        try {
            long endTime = System.currentTimeMillis();
            log.debug("RESPONSE : " + JSON.toJSONString(response));
            log.debug("COST : " + (endTime - startTime) + "ms");
        } catch (Exception e) {
            //swallow it
            log.error("logResponse error : " + e);
        }
    }

    private boolean isFlux(Class<?> type) {
        return type == Flux.class;
    }
    private boolean isMono(Class<?> type) {
        return type == Mono.class;
    }
    private boolean isReactor(Class<?> type) {
        return type == Mono.class || type == Flux.class;
    }

    private Class<?> getReactorReturnType(MethodSignature ms) {
        Type type = ms.getMethod().getGenericReturnType();
        if (type instanceof ParameterizedType) {
            final Type t = ((ParameterizedType) type).getActualTypeArguments()[0];
            final String name = t.getTypeName().split("<")[0];
            return returnTypeMap.computeIfAbsent(name, n -> {
                try {
                    return Class.forName(n);
                } catch (ClassNotFoundException e) {
                    log.error("ClassNotFoundException error : " + e.getMessage());
                    return null;
                }
            });
        }
        return null;
    }
}
